package com.nlp.mallet;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.math.BigDecimal;
import java.util.Iterator;
import java.util.Random;

import cc.mallet.classify.Classification;
import cc.mallet.classify.Classifier;
import cc.mallet.classify.ClassifierTrainer;
import cc.mallet.classify.MaxEntTrainer;
import cc.mallet.classify.Trial;
import cc.mallet.pipe.iterator.CsvIterator;
import cc.mallet.types.Instance;
import cc.mallet.types.InstanceList;
import cc.mallet.types.Label;
import cc.mallet.types.Labeling;

/**
 * 文档分类
 * @author ygsong.abcft
 *
 */
public class DocClassifier {
	
	
	 /**
     * 预测与评价
     */
    public String predict(Classifier classifier,Instance testInstance){
        Labeling labeling = classifier.classify(testInstance).getLabeling();
        Label label = labeling.getBestLabel();
        for (int rank = 0; rank < labeling.numLocations(); rank++) {
			System.out.print(labeling.getLabelAtRank(rank)+"："+labeling.getValueAtRank(rank)+"  ");
		}
        System.out.println();
        return (String)label.getEntry();
    }
	/**
	 * 实例化一个分类器
	 * @param trainInstances
	 * @return
	 */
	public Classifier trainClassifier(InstanceList trainInstances) {
		//这里我们使用最大熵（即多元逻辑回归）                                
        //分类器。Mallet包含各种分类                                    
        //算法，有关详细信息，请参阅JavaDoc API。        
		ClassifierTrainer<?> trainer = new MaxEntTrainer();
		Classifier classifier= trainer.train(trainInstances);
		return classifier;
	}

	/**
	 * 从文件中加载分类器
	 * @param serizlizedFile
	 * @return
	 */
	public Classifier loadClassifier(File serizlizedFile) {
		ObjectInputStream ois = null;
		try {
			ois = new ObjectInputStream(new FileInputStream(serizlizedFile));
			Classifier classifier = (Classifier) ois.readObject();
			return classifier;
		} catch (IOException | ClassNotFoundException e) {
			e.printStackTrace();
		}finally {
			if (ois != null) {
				try {
					ois.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		return null;
		
	}
	
	/**
	 * 把分类器保存到文件中
	 * @param classifier
	 * @param serializedFile
	 */
	public void saveClassifier(Classifier classifier, File serializedFile) {
		ObjectOutputStream oos = null;
		try {
			oos = new ObjectOutputStream(new FileOutputStream(serializedFile));
			oos.writeObject(classifier);
		} catch (IOException e) {
			e.printStackTrace();
		}finally {
			if (oos != null) {
				try {
					oos.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

	/**
	 * 使用经过训练的分类器来猜测新数据的类别。
	 * 我们首先从文件中读取原始实例数据，通过加载原始训练数据的相同管道传递数据，
	 * 然后通过分类器传递实例并打印分类分数。
	 * 请注意，在这个例子中，我们逐个读取实例，
	 * 而不在任何地方保存它们。这种基于流的方法节省内存，但也可以保持实例的正确性。
	 * 请参阅下一个示例，以了解这样做的一种方法。
	 * @param classifier
	 * @param file
	 */
	public void printLabelings(Classifier classifier, File file) {
		FileReader fileReader;
		try {
			//创建一个新的迭代器，它将读取原始实例数据文件的行。
			//行应格式化为：[name] [label] [data ... ]
			//在这种情况下，“lable”被忽略。
			fileReader = new FileReader(file);
			//String lineRegex = "(\\w+)\\s+(\\w+)\\s+(.*)";
			String lineRegex = "([\u4e00-\\u9fa5]+)\\s+([\u4e00-\\u9fa5]+)\\s+(.*)";
			CsvIterator reader = new CsvIterator(fileReader,lineRegex,3,2,1);
			
			//创建一个迭代器，并通过每个实例的迭代器,创建训练数据的相同管道分类器。
			Iterator<Instance> instances = classifier.getInstancePipe().newIteratorFrom(reader);
			//Classifier.classify() Classification
			//包括实例、分类器和分类结果（label）。
			//这里我们只关心label。
			while (instances.hasNext()) {
				Classification classify = classifier.classify(instances.next());
				Labeling labeling = classify.getLabeling();
				//按weight顺序打印标签
				for (int rank = 0; rank < labeling.numLocations(); rank++) {
					System.out.println(labeling.getLabelAtRank(rank)+"："+labeling.getValueAtRank(rank)+"  ");
				}
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * 为了知道分类器是否产生可靠的预测，
	 * 我们可以通过提供附加的标记数据并将预测的标签与实际的标签进行比较来测试。
	 * 对于这个示例，我们从文件中读取测试实例，并报告几个评估度量，
	 * 包括准确性、精确性、召回和F-.。
	 * @param classifier
	 * @param file
	 * @throws Exception
	 */
	public void eveluate(Classifier classifier,File file) throws Exception {
		//创建一个包含测试数据的实例集。
		//为了保证兼容性，过程实例用管道进行原始训练实例。
		InstanceList testInstances = new InstanceList(classifier.getInstancePipe());
		
		//创建一个新的迭代器，它将读取原始实例数据文件的行。
		//行应格式化为：[name] [label] [data ... ]  
		CsvIterator reader = new CsvIterator(new FileReader(file), "(\\w+)\\s+(\\w+)\\s+(.*)", 3, 2, 1);
		
		//将迭代器加载的所有实例添加到我们的实例列表，传递原始输入数据
		//通过分类器的原始输入管道。
		testInstances.addThruPipe(reader);
		
		Trial trial = new Trial(classifier,testInstances);
		
		//这个训练实现了多指标的评价标准。看到更多的细节，Javadoc API
		System.out.println("Accuracy:"+trial.getAccuracy());
		//精度、召回和F1是针对特定的类计算的，可以通过对象（通常是String）或类的整数ID来标识
		System.out.println("F1 for class 'good'："+trial.getF1("good"));
		System.out.println("Precision for class '"+classifier.getLabelAlphabet().lookupIndex(1));
		
	}
	
	/**
	 * 为了执行N倍交叉验证，我们需要产生多个随机分裂的数据到测试和训练集。
	 * 此示例演示如何执行一个这样的拆分，返回可用于报告评估度量的试用对象。
	 * @param instances
	 * @return
	 */
	public Trial testTrainSplit(InstanceList instances) {
		int TRAINING = 0;
		int TESTING = 1;
		int VALIDATION = 2;
		//将输入列表分成训练（90%）和测试（10%）列表。
		//通过创建列表的副本来进行划分，
		//随机复制副本，然后分配
		//基于所提供的比例对每个子列表进行实例。
		InstanceList[] instanceLists = instances.split(new Random(), new double[] {0.9,0.1,0.0});
		
		//第三位是“验证”集，
		//这是一组未直接使用的实例。
		//用于培训，但可用于确定
		//何时停止训练和估计最优值
		//骚扰参数的设置。
		//大多数Mallet ClassifierTrainers目前无法利用
		//验证集。
		Classifier classifier = trainClassifier(instanceLists[TRAINING]);
		Trial trial = new Trial(classifier, instanceLists[TESTING]);
		//这个训练实现了多指标的评价标准。看到更多的细节，Javadoc API
				System.out.println("准确度："+trial.getAccuracy());
				//精度、召回和F1是针对特定的类计算的，可以通过对象（通常是String）或类的整数ID来标识
				System.out.println("'健康'这个分类的F1得分："+trial.getF1("健康"));

				System.out.println("'健康'这个分类的F1得分："+trial.getF1("健康"));

				double precision = trial.getPrecision("健康");
				double recall = trial.getRecall("健康");
				System.out.println("精确率[precision]="+precision);
				System.out.println("召回率[recall]="+recall);
				/*double f1 = (1+Math.pow(1,2))*(precision*recall)/((Math.pow(1,2))*precision+recall);
				double f2 = (1+Math.pow(2,2))*(precision*recall)/((Math.pow(2,2))*precision+recall);
				double f = (1+Math.pow(0.5,2))*(precision*recall)/((Math.pow(0.5,2))*precision+recall);*/
				
				double[] dd = {1,2,0.5};
				for (double d : dd) {
					double u = (1+Math.pow(d,2))*(precision*recall);
					double l = ((Math.pow(d,2))*precision+recall);
					//System.out.println(u);
					//System.out.println(l);
					if (l <= 0.0) {
						l = 1;
					}
					try {
						BigDecimal f = new BigDecimal(u).divide(new BigDecimal(l)).setScale(4, BigDecimal.ROUND_HALF_UP);
						 
						System.out.println("F"+d+"="+f.doubleValue());
					} catch (Exception e) {
						System.out.println(l);
						e.printStackTrace();
					}
					
				}
				
				
				
				

				//System.out.println("'健康'这个分类的精密度："+classifier.getLabelAlphabet().lookupIndex("健康"));
		
				return new Trial(classifier, instanceLists[TESTING]);
	}
}
